<!--
 (c) 2016 Rob Wu <rob@robwu.nl>
 This Source Code Form is subject to the terms of the Mozilla Public
 License, v. 2.0. If a copy of the MPL was not distributed with this
 file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<body> Open the console and check whether there are errors.
<script src="search-tools.js"></script>
<script>
function assertEq(expected, actual, descriptionPrefix) {
    if (expected !== actual) {
        console.error(`${descriptionPrefix}
Expected: ${expected}.
Actual  : ${actual}.`);
    }
}

// - line:col - line:col   -- if a result was expected
// - null                  -- if no result is expected
function serializeResult(r) {
    if (!r) {
        return `${r}`; // null, undefined, ...?
    }
    return `${r.lineStart}:${r.columnStart} - ${r.lineEnd}:${r.columnEnd}`;
}

// Search in |text| for |query|. |expected| is a string where each line
// specifies an expectation, in the format of serializeResult.
function testQuery(text, query, expected) {
    // trim to allow test expectations to be written on separate lines.
    expected = expected.trim();

    let se = new SearchEngineLogic(text);
    se.setQuery(query);

    let actual = Array.from(se.runQuery(), serializeResult).join('\n');
    assertEq(expected, actual, `Testing ${query} with ${JSON.stringify(text)}`);
}

// opAndExpectation is a string where each line has the following format:
// op = result (where result matches the return value of serializeResult).
function testSearchOps(text, query, opAndExpectations) {
    const descriptionPrefix = `Testing ${query} with ${JSON.stringify(text)}`;

    let se = new SearchEngineLogic(text);
    se.setQuery(query);

    for (let opAndExpectation of opAndExpectations.trim().split(/\n+/)) {
        let [op, expected] = opAndExpectation.split(' = ')
        let actual = serializeResult(se[op]());
        assertEq(expected, actual, `${descriptionPrefix}, ${op}`);
    }
}

function testGetText(text, lineStart, columnStart, lineEnd, columnEnd, expect) {
    let se = new SearchEngineLogic(text);
    let actual = se.getText(lineStart, columnStart, lineEnd, columnEnd);
    assertEq(expect, actual,
            `getText of ${lineStart}:${columnStart} - ${lineEnd}:${columnEnd}`);
}


console.log('Running tests');
</script>

<script>
console.log('Testing *runQuery()');

// Always matches
testQuery('x', /(?:)/, ``);
testQuery('x', /x*?/, ``);
// Never matches
testQuery('x', /(?!)/, ``);
// Matches
testQuery('x', /x/, `0:0 - 0:1`);
testQuery('ax', /x/, `0:1 - 0:2`);
testQuery('axc', /x/, `0:1 - 0:2`);

// Case-sensitive
testQuery('x', /X/, ``);
testQuery('X', /x/, ``);
testQuery('X', /X/, `0:0 - 0:1`);

// Case-insensitive
testQuery('x', /X/i, `0:0 - 0:1`);
testQuery('X', /x/i, `0:0 - 0:1`);
testQuery('X', /X/i, `0:0 - 0:1`);

// Non-matching line after match
testQuery('x\njunk', /x/, `0:0 - 0:1`);
testQuery('ax\njunk', /x/, `0:1 - 0:2`);
testQuery('axc\njunk', /x/, `0:1 - 0:2`);

// Non-matching line before match
testQuery('junk\nx', /x/, `1:0 - 1:1`);
testQuery('junk\nax', /x/, `1:1 - 1:2`);
testQuery('junk\naxc', /x/, `1:1 - 1:2`);

// Multi-line match
testQuery('a\nb', /a\nb/, `0:0 - 1:1`);
testQuery('\nb', /\nb/, `0:0 - 1:1`);
testQuery('a\n', /a\n/, `0:0 - 1:0`);
testQuery('\n', /\n/, `0:0 - 1:0`);

// Multi-line match with non-matching text after match
testQuery('a\nbjunk', /a\nb/, `0:0 - 1:1`);
testQuery('\nbjunk', /\nb/, `0:0 - 1:1`);
testQuery('a\njunk', /a\n/, `0:0 - 1:0`);
testQuery('\njunk', /\n/, `0:0 - 1:0`);

// Multi-line match with non-matching text before match
testQuery('junka\nb', /a\nb/, `0:4 - 1:1`);
testQuery('junk\nb', /\nb/, `0:4 - 1:1`);
testQuery('junka\n', /a\n/, `0:4 - 1:0`);
testQuery('junk\n', /\n/, `0:4 - 1:0`);

// Multi-line match with non-matching line after match
testQuery('a\nb\njunk', /a\nb/, `0:0 - 1:1`);
testQuery('\nb\njunk', /\nb/, `0:0 - 1:1`);
testQuery('a\n\njunk', /a\n/, `0:0 - 1:0`);
testQuery('\n\njunk', /\n/, `
0:0 - 1:0
1:0 - 2:0
`);

// Multi-line match with non-matching line before match
testQuery('junk\na\nb', /a\nb/, `1:0 - 2:1`);
testQuery('junk\n\nb', /\nb/, `1:0 - 2:1`);
testQuery('junk\na\n', /a\n/, `1:0 - 2:0`);
testQuery('junk\n\n', /\n/, `
0:4 - 1:0
1:0 - 2:0
`);

// 3-line match
testQuery(' \n \n ', /\s+/, `0:0 - 2:1`);
testQuery(' \n \n b', /\s+/, `0:0 - 2:1`);
testQuery('a \n \n ', /\s+/, `0:1 - 2:1`);
testQuery('a \n \n ', /[\S\s]+/, `0:0 - 2:1`);

// .* matches should only output non-empty matches
testQuery('\nb\n', /.*/, `1:0 - 1:1`);
testQuery('a\nb\nc', /.*/, `
0:0 - 0:1
1:0 - 1:1
2:0 - 2:1
`);

</script>

<script>
console.log('Testing findNext and findPrev');

// No results
testSearchOps('x', /nomatch/, `
findNext = null
findPrev = null
`);
testSearchOps('x', /nomatch/, `
findPrev = null
findNext = null
`);
testSearchOps('x\ny', /(?:)/, `
findPrev = null
findNext = null
`);
testSearchOps('x\ny', /(?:)/, `
findNext = null
findPrev = null
`);

// Find a result.
testSearchOps('ax\nyb\nczd', /a/, `findNext = 0:0 - 0:1`);
testSearchOps('ax\nyb\nczd', /x/, `findNext = 0:1 - 0:2`);
testSearchOps('ax\nyb\nczd', /y/, `findNext = 1:0 - 1:1`);
testSearchOps('ax\nyb\nczd', /b/, `findNext = 1:1 - 1:2`);
testSearchOps('ax\nyb\nczd', /c/, `findNext = 2:0 - 2:1`);
testSearchOps('ax\nyb\nczd', /z/, `findNext = 2:1 - 2:2`);
testSearchOps('ax\nyb\nczd', /d/, `findNext = 2:2 - 2:3`);
testSearchOps('ax\nyb\nczd', /\n/, `findNext = 0:2 - 1:0`);

// Find, wrap around, find backwards.
testSearchOps('ax\nyb\nczd', /[xyz]/, `
findNext = 0:1 - 0:2
findNext = 1:0 - 1:1
findNext = 2:1 - 2:2

findNext = 0:1 - 0:2
findPrev = 2:1 - 2:2
findPrev = 1:0 - 1:1
findPrev = 0:1 - 0:2
`);
// Find backwards, wrap around, find forwards.
testSearchOps('ax\nyb\nczd', /[xyz]/, `
findPrev = 2:1 - 2:2
findPrev = 1:0 - 1:1
findPrev = 0:1 - 0:2

findPrev = 2:1 - 2:2
findNext = 0:1 - 0:2
findNext = 1:0 - 1:1
findNext = 2:1 - 2:2
`);

// Find adjacent results (forwards, wrap, backwards).
testSearchOps('aaa', /a/, `
findNext = 0:0 - 0:1
findNext = 0:1 - 0:2
findNext = 0:2 - 0:3

findNext = 0:0 - 0:1
findPrev = 0:2 - 0:3
findPrev = 0:1 - 0:2
findPrev = 0:0 - 0:1
`);
// Find adjacent results (backwards, wrap, forwards).
testSearchOps('aaa', /a/, `
findNext = 0:0 - 0:1
findNext = 0:1 - 0:2
findNext = 0:2 - 0:3

findNext = 0:0 - 0:1
findPrev = 0:2 - 0:3
findPrev = 0:1 - 0:2
findPrev = 0:0 - 0:1
`);

// Find forwards/backwards when there are multiple possible good results.
// Note: In the backwards case, for a non-RegExp search returning 0:1 - 0:3
// would be more usual, but since backwards search with a RegExp is not
// efficiently possible in JS, we just use the same results as forwards search,
// but then in the other direction.
testSearchOps('aaa', /aa/, `
findNext = 0:0 - 0:2
findNext = 0:0 - 0:2
findPrev = 0:0 - 0:2
findPrev = 0:0 - 0:2
`);
testSearchOps('aaa', /aa/, `
findPrev = 0:0 - 0:2
findPrev = 0:0 - 0:2
findNext = 0:0 - 0:2
findNext = 0:0 - 0:2
`);
// Same as above, but now with two results to see progression in results.
testSearchOps('aaaaa', /aa/, `
findNext = 0:0 - 0:2
findNext = 0:2 - 0:4
findNext = 0:0 - 0:2
findPrev = 0:2 - 0:4
findPrev = 0:0 - 0:2
findPrev = 0:2 - 0:4
`);
testSearchOps('aaaaa', /aa/, `
findPrev = 0:2 - 0:4
findPrev = 0:0 - 0:2
findPrev = 0:2 - 0:4
findNext = 0:0 - 0:2
findNext = 0:2 - 0:4
findNext = 0:0 - 0:2
`);
</script>

<script>
console.log('getMinimumResultCount and hasFoundAllResults');
(() => {
let text = 'a'.repeat(1001);
let se = new SearchEngineLogic(text);
se.setQuery(/a/);
function assertResultCountEq(limitTo, expected) {
    let actual = se.getMinimumResultCount(limitTo);
    assertEq(expected, actual, `getMinimumResultCount(limitTo = ${limitTo})`);
}
function assertHasFoundAllResults(expected) {
    let actual = se.hasFoundAllResults();
    assertEq(expected, actual, `hasFoundAllResults()`);
}

assertHasFoundAllResults(false);
assertResultCountEq(0, 0);
assertResultCountEq(1, 1);
assertResultCountEq(2, 2);
// Once known, results do not disappear (even if limitTo is very low).
assertResultCountEq(0, 2);
// If not specified, yield the first 100 results.
assertResultCountEq(undefined, 100);
// Once known, results do not disappear.
assertResultCountEq(0, 100);
assertHasFoundAllResults(false);

se.setQuery(/a/);
// Results must be reset upon resetting the query.
assertResultCountEq(0, 0);
se.findNext();
// findNext should find one result only.
assertResultCountEq(0, 1);
assertHasFoundAllResults(false);
se.findPrev();
// findPrev wrapped around and has to find all results to obtain the last one.
assertResultCountEq(0, 1001);
assertHasFoundAllResults(true);

// Results must be reset again.
se.setQuery(/a/);
assertHasFoundAllResults(false);
assertResultCountEq(0, 0);
})();
</script>

<script>
console.log('Running getText');
// Empty match
testGetText('x', 0, 0, 0, 0, '');
testGetText('x', 0, 1, 0, 1, '');
testGetText('x\ny', 1, 0, 1, 0, '');
testGetText('x\ny', 1, 1, 1, 1, '');
// Some match
testGetText('x', 0, 0, 0, 1, 'x');
testGetText('abc', 0, 1, 0, 2, 'b');
testGetText('abc', 0, 1, 0, 3, 'bc');
// Multiple lines.
testGetText('x\na\nb\nc\ny', 1, 0, 1, 1, 'a');
testGetText('x\na\nb\nc\ny', 2, 0, 2, 1, 'b');
testGetText('x\na\nb\nc\ny', 1, 0, 2, 0, 'a\n');
testGetText('x\na\nb\nc\ny', 1, 0, 2, 1, 'a\nb');

// Invalid ranges, let's see what happens.
// (apparently an empty string is returned. OK?).
testGetText('x', 0, 0, 0, -1, '');
testGetText('x', 0, 0, -1, 0, '');
testGetText('x', 0, -1, 0, 0, '');
testGetText('x', -1, 0, 0, 0, '');
testGetText('x', -1, -1, -1, -1, '');
testGetText('x', 2, 2, 2, 2, '');
</script>

<script>
console.log('binarySearch');
function testBinarySearch(array, needle, expectedF, expectedC) {
    // needle === elem ->  0
    // needle > elem   -> >0
    // needle < elem   -> <0
    let evaluate = elem => needle - elem;

    // If not found, round down.
    let actualF = binarySearch(array, evaluate, false);
    assertEq(expectedF, actualF, `binarySearch ${array} for ${needle}`);

    // If not found, round up.
    let actualC = binarySearch(array, evaluate, true);
    assertEq(expectedC, actualC, `binarySearch ${array} for ${needle}`);
}
// Empty list
testBinarySearch([], 42, 0, 0);

// Exact match (42 == 42).
testBinarySearch([42], 42, 0, 0);
testBinarySearch([42, 51], 42, 0, 0);
testBinarySearch([42, 52, 53], 42, 0, 0);
testBinarySearch([42, 52, 53, 54], 42, 0, 0);
testBinarySearch([11, 42], 42, 1, 1);
testBinarySearch([11, 12, 42], 42, 2, 2);
testBinarySearch([11, 12, 13, 42], 42, 3, 3);
testBinarySearch([11, 12, 42, 51], 42, 2, 2);
testBinarySearch([11, 42, 51, 52], 42, 1, 1);
testBinarySearch([42, 51, 52, 53], 42, 0, 0);

// No exact match (42 > 41).
testBinarySearch([41], 42, 0, 0);
testBinarySearch([41, 51], 42, 0, 1);
testBinarySearch([41, 52, 53], 42, 0, 1);
testBinarySearch([41, 52, 53, 54], 42, 0, 1);
testBinarySearch([11, 41], 42, 1, 1);
testBinarySearch([11, 12, 41], 42, 2, 2);
testBinarySearch([11, 12, 13, 41], 42, 3, 3);
testBinarySearch([11, 12, 41, 51], 42, 2, 3);
testBinarySearch([11, 41, 51, 52], 42, 1, 2);
testBinarySearch([41, 51, 52, 53], 42, 0, 1);

// No exact match (42 < 43).
testBinarySearch([43], 42, 0, 0);
testBinarySearch([43, 51], 42, 0, 0);
testBinarySearch([43, 52, 53], 42, 0, 0);
testBinarySearch([43, 52, 53, 54], 42, 0, 0);
testBinarySearch([11, 43], 42, 0, 1);
testBinarySearch([11, 12, 43], 42, 1, 2);
testBinarySearch([11, 12, 13, 43], 42, 2, 3);
testBinarySearch([11, 12, 43, 51], 42, 1, 2);
testBinarySearch([11, 43, 51, 52], 42, 0, 1);
testBinarySearch([43, 51, 52, 53], 42, 0, 0);
</script>

<script>
console.log('Tests finished');
</script>
